/* SPDX-License-Identifier: MIT */

package atlantafx.sampler.page.general;

import static java.nio.charset.StandardCharsets.UTF_8;

import atlantafx.base.util.PlatformUtils;
import atlantafx.sampler.theme.SamplerTheme;
import atlantafx.sampler.theme.SceneBuilderTheme;
import org.jspecify.annotations.Nullable;

import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Objects;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

final class SceneBuilderInstaller {

    private static final String CONFIG_FILE_NAME = "SceneBuilder.cfg";
    private static final String THEME_PACK_FILE_NAME = "atlantafx-scene-builder.zip";
    private static final char CLASSPATH_SEPARATOR = PlatformUtils.isWindows() ? ';' : ':';

    private final Path sceneBuilderDir;
    private @Nullable Path configDir;

    public SceneBuilderInstaller(Path dir) {
        this.sceneBuilderDir = Objects.requireNonNull(dir);
    }

    public boolean hasUserWritePermission() {
        return Files.isWritable(getConfigDir()) && Files.isWritable(getConfigFile());
    }

    public boolean isValidDir() {
        var cfgDir = getConfigDir();
        var cfgFile = getConfigFile();
        return Files.exists(cfgDir) && Files.isDirectory(cfgDir)
            && Files.exists(cfgFile) && Files.isRegularFile(cfgFile);
    }

    public boolean isThemePackInstalled() {
        try {
            String cfg = Files.readString(getConfigFile(), UTF_8);
            Path themePack = getThemePack();
            return cfg.contains(THEME_PACK_FILE_NAME) && Files.exists(themePack) && Files.isRegularFile(themePack);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @SuppressWarnings("StringSplitter")
    public void install(Map<SceneBuilderTheme, SamplerTheme> themes) {
        Objects.requireNonNull(themes);

        if (themes.isEmpty()) {
            return;
        }

        // write theme pack archive to install dir
        File zipFile = getThemePack().toFile();
        try (var fos = new FileOutputStream(zipFile);
             var bos = new BufferedOutputStream(fos);
             var out = new ZipOutputStream(bos)) {

            var readme = new StringBuilder();
            readme
                .append("This file was auto-generated by AtlantaFX Sampler v")
                .append(System.getProperty("app.version"))
                .append(".\n\n")
                .append("Installed themes:\n\n");

            for (var theme : themes.entrySet()) {
                var zipPath = theme.getKey().url();

                readme
                    .append(String.format("%-45s", theme.getKey().name()))
                    .append("  >>  ")
                    .append(theme.getValue().getName())
                    .append("\n");

                writeToZip(out, zipPath, theme.getValue().getResource().getInputStream());
            }

            writeToZip(out, "readme.txt", new ByteArrayInputStream(readme.toString().getBytes(UTF_8)));
        } catch (Exception e) {
            throw new RuntimeException("Unable to write theme pack to the SceneBuilder installation directory.", e);
        }

        // update config file
        try {
            List<String> cfgData = Files.readAllLines(getConfigFile(), UTF_8);

            // already updated
            if (cfgData.stream().anyMatch(s -> s.contains(THEME_PACK_FILE_NAME))) {
                return;
            }

            backupConfig();

            ListIterator<String> it = cfgData.listIterator();
            while (it.hasNext()) {
                var line = it.next();
                if (line != null && line.startsWith("app.classpath")) {
                    var kv = line.split("=");

                    if (kv.length != 2) {
                        throw new RuntimeException("Unexpected value in SceneBuilder config file: \"" + line + "\".");
                    }

                    it.set(kv[0] + "=$APPDIR" + File.separator + THEME_PACK_FILE_NAME + File.pathSeparator + kv[1]);
                }
            }

            Files.writeString(
                getConfigFile(),
                String.join("\n", cfgData), // System.lineSeparator() ?
                StandardOpenOption.CREATE,
                StandardOpenOption.TRUNCATE_EXISTING
            );
        } catch (Exception e) {
            throw new RuntimeException("Unable to update SceneBuilder config file.", e);
        }
    }

    private void writeToZip(ZipOutputStream out, String zipPath, InputStream in) throws IOException {
        var entry = new ZipEntry(zipPath);
        try (in) {
            out.putNextEntry(entry);

            byte[] bytes = new byte[1024];
            int count = in.read(bytes);
            while (count > -1) {
                out.write(bytes, 0, count);
                count = in.read(bytes);
            }
        } finally {
            out.closeEntry();
        }
    }

    public void uninstall() {
        var cfg = getConfigFile();
        var backup = getBackupConfigFile();

        // rollback config file
        if (Files.exists(backup)) {
            // easy way
            copyFile(backup, cfg, StandardCopyOption.REPLACE_EXISTING);
        } else {
            // fallback (probably not needed, but should do no harm)
            try {
                List<String> cfgData = Files.readAllLines(getConfigFile(), UTF_8);

                // not present
                if (cfgData.stream().noneMatch(s -> s.contains(THEME_PACK_FILE_NAME))) {
                    return;
                }

                ListIterator<String> it = cfgData.listIterator();
                while (it.hasNext()) {
                    var line = it.next();
                    if (line != null && line.startsWith("app.classpath")) {
                        it.set(line.replace("$APPDIR" + File.separator + THEME_PACK_FILE_NAME + CLASSPATH_SEPARATOR, ""));
                    }
                }

                Files.writeString(
                    getConfigFile(),
                    String.join("\n", cfgData),  // System.lineSeparator() ?
                    StandardOpenOption.CREATE,
                    StandardOpenOption.TRUNCATE_EXISTING
                );
            } catch (Exception e) {
                throw new RuntimeException("Unable to update SceneBuilder config file.", e);
            }
        }

        // remove theme pack and backup
        deleteFile(getThemePack());
        deleteFile(backup);
    }

    void backupConfig() {
        var cfg = getConfigFile();
        var backup = getBackupConfigFile();

        if (!Files.exists(backup)) {
            copyFile(cfg, backup);
        }
    }

    private void copyFile(Path source, Path dest, StandardCopyOption... options) {
        Objects.requireNonNull(source);
        Objects.requireNonNull(dest);

        try {
            Files.copy(source, dest, options);
        } catch (IOException e) {
            throw new RuntimeException("Unable to copy \"" + source + "\" to \"" + dest + "\".", e);
        }
    }

    public static void deleteFile(Path path) {
        Objects.requireNonNull(path);

        try {
            if (Files.exists(path)) {
                Files.delete(path);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public static Path getDefaultConfigDir() {
        if (PlatformUtils.isWindows()) {
            return Path.of(System.getProperty("user.home"), "AppData", "Local", "SceneBuilder");
        } else if (PlatformUtils.isLinux()) {
            return Path.of("/opt/scenebuilder/");
        } else if (PlatformUtils.isUnix()) {
            return Path.of("/Applications/SceneBuilder.app/");
        } else {
            return Path.of(".");
        }
    }

    private Path getConfigDir() {
        if (configDir != null) {
            return configDir;
        }

        // app image structure is documented here
        // https://docs.oracle.com/en/java/javase/20/jpackage/packaging-overview.html
        Path dir = sceneBuilderDir;
        if (PlatformUtils.isWindows()) {
            dir = sceneBuilderDir.resolve("app");
        } else if (PlatformUtils.isMac()) {
            dir = sceneBuilderDir.resolve("Contents/app");
        } else if (PlatformUtils.isUnix()) {
            dir = sceneBuilderDir.resolve("lib/app");
        }

        // last chance, if app image has an unknown structure
        if (!Files.exists(dir.resolve(CONFIG_FILE_NAME))) {
            try (var stream = Files.walk(sceneBuilderDir)) {
                dir = stream
                    .filter(f -> Objects.equals(f.getFileName().toString(), CONFIG_FILE_NAME))
                    .findAny()
                    .map(Path::getParent)
                    .orElse(null);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        this.configDir = dir;

        return Objects.requireNonNullElse(dir, sceneBuilderDir);
    }

    private Path getConfigFile() {
        return getConfigDir().resolve(CONFIG_FILE_NAME);
    }

    private Path getBackupConfigFile() {
        return getConfigDir().resolve("SceneBuilder.cfg.atlantafx.backup");
    }

    private Path getThemePack() {
        return getConfigDir().resolve(THEME_PACK_FILE_NAME);
    }
}
